/*Author: Rony Lahav (ID:043489889)*/

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.Collection;
import java.util.HashMap;
import java.util.SortedMap;
import java.util.TreeMap;


public class SNsHandler {

	static SNsHandler instance;
	private static DatagramSocket ds = final_project.mainUDPSoc;
	String snListMsg;
	SortedMap<String, DBTuple> list=null;
	
	private SNsHandler(){
		
	}
	//singleton
	public static SNsHandler getInstance() {
		if (instance==null)
			instance = new SNsHandler();
		return instance;
	}
	
	public static void propagateMessageToMySNs(String msg, String originSNKey){
		msg = "#" + msg;
		SortedMap<String, DBTuple> sns = DataBase.getInstance().getOnlineSNTuplesForPropagation();
		Collection<DBTuple> c = sns.values();
		for (DBTuple snTuple : c) {
			if (final_project.isShutDown()) {
				break;
			}
			if (!snTuple.getKey().equals(originSNKey)){
				sendMsgToOneTuple(snTuple, msg);
			}
		}
	}
	
	public static void propagateMessageToMyPeers(String msg, String originPeerID){
		SortedMap<String, DBTuple> tuples = DataBase.getInstance().getAllOnlineRegTuples();
		Collection<DBTuple> c = tuples.values();
		for (DBTuple tuple : c) {
			if (final_project.isShutDown()) {
				break;
			}
			if (originPeerID==null || (originPeerID!=null && !tuple.getPeerID().equals(originPeerID))){
				sendMsgToOneTuple(tuple, msg);
			}
		}
	}
	
	private static void UDPSend(DBTuple tupleForSending, String msg) {
		try {
			if (!final_project.isShutDown()){
				DatagramPacket dp = new DatagramPacket(msg.getBytes(), msg.length(), InetAddress.getByName(tupleForSending.getPeerIP()),Integer.parseInt(tupleForSending.getUdpPort()));
				ds.send(dp); // send the message to the peer
			}
		}
		catch (SocketTimeoutException e1){
			if (ds != null && !ds.isClosed()){
				ds.close();
			}
		}
		catch (IOException e2){}
	}
	
	protected static void sendMsgToOneTuple(DBTuple tuple, String msg){
		// Open a dedicated thread to send FT to one specific peer
		SingleUDPMessageSender singleMsg = new SingleUDPMessageSender(final_project.mainUDPSoc, tuple, msg, final_project.getNewUniqueID());
		Thread thread = new Thread(singleMsg);
		final_project.addThreadToLivingList(singleMsg.getUniqueID(), thread);
		thread.start();
	}
	
	public boolean diagnoseSNList(String msg,String senderIP,int senderPort,boolean replySNLIST){
		String senderKey = null;
		DBTuple senderTuple = null;
		if (replySNLIST){	//SN has agreed to connect with me
			//mark it as online and as directly connected to me
			senderKey = senderIP+":"+senderPort;
			senderTuple = DataBase.getInstance().findSN(senderKey);
			senderTuple.setStatus("1");
			senderTuple.setLastConnectionTime(System.currentTimeMillis());
			senderTuple.setConnectedThrough(final_project.getMyKey());
			DataBase.getInstance().updateSN(senderTuple);
			//update new/newer records of his SNLIST into my DB
			String[] msgLines = msg.split(MessageCenter.newline);
			if (msgLines[0].endsWith(MessageCenter.newline))
				msgLines[0]=msgLines[0].substring(0, msgLines[0].length() - MessageCenter.newline.length());
			if (msgLines.length < 2)
				return false;
			for (int i=1;i<msgLines.length;++i){
				String peerData = msgLines[i].split(": ")[1];
				String[] peerIPPortLink = peerData.split(",");
				DBTuple snTuple = new DBTuple();
				snTuple.setPeerIP(peerIPPortLink[0]);
				snTuple.setUdpPort(peerIPPortLink[1]);
				snTuple.setConnectedThrough(peerIPPortLink[2]);
				snTuple.setStatus("0");
				snTuple.setIsSN(true);
				snTuple.setLastConnectionTime(System.currentTimeMillis());
				DataBase.getInstance().updateSN(snTuple);
			}
			return false;
		}
		else{
			boolean reply = true;
			String[] msgLines = msg.split(MessageCenter.newline);
			if (msgLines[0].endsWith(MessageCenter.newline))
				msgLines[0]=msgLines[0].substring(0, msgLines[0].length() - MessageCenter.newline.length());
			if (msgLines.length < 2)
				return false;
			SortedMap<String, DBTuple> snList = new TreeMap<String, DBTuple>();
			boolean alreadyConnected = false;	//connected to one of the SNs i'm connected to
			//each line is an SN
			for (int i=1;i<msgLines.length && !alreadyConnected;++i){
				String peerData = msgLines[i].split(": ")[1];
				String[] peerIPPortLink = peerData.split(",");
				DBTuple newSN = new DBTuple();
				newSN.setPeerIP(peerIPPortLink[0]);
				newSN.setUdpPort(peerIPPortLink[1]);
				newSN.setConnectedThrough(peerIPPortLink[2]);
				if (!newSN.getConnectedThrough().equals("0")){	//if not "0" then equals senderKay
					DBTuple existingSN = DataBase.getInstance().findSN(newSN.getKey());
					if (existingSN!=null && !existingSN.getConnectedThrough().equals("0")){
						//i am connected to an SN that the sender is also already connected to => not minimal spanning tree 
						alreadyConnected = true;
					}
				}
				if (!alreadyConnected){
					newSN.setStatus("0");
					newSN.setIsSN(true);
					newSN.setLastConnectionTime(System.currentTimeMillis());
					snList.put(newSN.getKey(),newSN);
				}
			}
			if (!alreadyConnected){	//OK to connect to sender
				reply=true;	//send my SNLIST as reply
				//update sender as valid connected through SN
				senderKey = senderIP+":"+senderPort;
				senderTuple = DataBase.getInstance().findSN(senderKey);
				if (senderTuple==null){	//new SN to list
					DBTuple newSN = new DBTuple();
					newSN.setPeerIP(senderIP);
					newSN.setUdpPort(Integer.toString(senderPort));
					newSN.setIsSN(true);
					DataBase.getInstance().insertSN(newSN);
					senderTuple = DataBase.getInstance().findSN(senderKey);
				}
				senderTuple.setStatus("1");
				senderTuple.setLastConnectionTime(System.currentTimeMillis());
				senderTuple.setConnectedThrough(final_project.getMyKey());
				DataBase.getInstance().updateSN(senderTuple);
				//update all other new/newer record from sender's SNLIST
				Collection<DBTuple> c = snList.values();
				for (DBTuple sn : c) {
					DataBase.getInstance().updateSN(sn);
				}
			}
			else
				snList.clear();
			return reply;
		}
	}
	
	public void firstConnectionToSNs(){
		list = DataBase.getInstance().getSnList();
		if (list != null){
			goThroughList();
		}
		
	}
	
	private void goThroughList(){
		DBTuple sn = DataBase.getInstance().getNextUnconnectedSNTuple();
		while (sn!=null){
			UDPSend(sn);
			listenForSnList(sn);
			sn = DataBase.getInstance().getNextUnconnectedSNTuple();
		}
		DataBase.getInstance().clearUnconnectedList();
	}
	
	private void UDPSend(DBTuple tupleForSending) {
		try {
			snListMsg = "#" + MessageCenter.getInstance().createMessage_SNLIST_ForSNs(false);
			if (!final_project.isShutDown()){
				DatagramPacket dp = new DatagramPacket(snListMsg.getBytes(), snListMsg.length(), InetAddress.getByName(tupleForSending.getPeerIP()),Integer.parseInt(tupleForSending.getUdpPort()));
				ds.send(dp); // send the message to the peer
			}
		}
		catch (SocketTimeoutException e1){
			if (ds != null && !ds.isClosed()){
				ds.close();
			}
		}
		catch (IOException e2){}
	}
	
	private void listenForSnList(DBTuple tupleForSending){
		boolean isFromSN = false;
		boolean avFromSN = false;
		boolean replySNLIST = false;
		try	
		{
			if (!final_project.isShutDown()){
				ds.setSoTimeout(final_project.timeout);
				// Preparing the buffer for receiving a message
				byte[] buffer = new byte[final_project.maxBufferSize];
				DatagramPacket rcvPacket = new DatagramPacket(buffer, buffer.length);
				ds.receive(rcvPacket);
				//Get sender details and message and start the processing thread
				int senderPort = rcvPacket.getPort();
				String senderIP = rcvPacket.getAddress().getHostAddress();
				String recievedMsg =  new String(rcvPacket.getData(),0,rcvPacket.getLength());
				isFromSN = recievedMsg.startsWith("#");
				if (isFromSN)
					recievedMsg = recievedMsg.substring(1);
				avFromSN = recievedMsg.startsWith("#");	//2 "#" mean the SN has no peers and it sent its own AV message
				if (avFromSN)
					recievedMsg = recievedMsg.substring(1);
				replySNLIST = recievedMsg.startsWith("$");
				if (replySNLIST)
					recievedMsg = recievedMsg.substring(1);
				if (replySNLIST)
					diagnoseSNList(recievedMsg,senderIP,senderPort,true);
				else{	//got a "surprise" message instead of SNLIST as reply
					if (avFromSN)
						recievedMsg = "#" + recievedMsg;
					if (isFromSN)
						recievedMsg = "#" + recievedMsg;
					UDPListenerProcessMessage udpProcMsg = new UDPListenerProcessMessageSN(senderIP,senderPort,recievedMsg);
					Thread thread = new Thread(udpProcMsg);
					final_project.addThreadToLivingList(final_project.getNewUniqueID(), thread);
					thread.start();
				}
			}
		}
		catch(SocketTimeoutException e1){}
		catch(IOException e2){}
	}
}
